﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media;

namespace Silverlight.Controls
{
    /// <summary>
    /// Represents a service that provides static methods to display a tooltip.
    /// </summary>
    public static class ToolTipService
    {
        private static readonly Dictionary<DependencyObject, ToolTipTimer> ElementsAndTimers =
            new Dictionary<DependencyObject, ToolTipTimer>();

        private static readonly Dictionary<DependencyObject, ToolTip> ElementsAndToolTips =
            new Dictionary<DependencyObject, ToolTip>();

        private static ToolTip _currentToolTip;
        private static UIElement _currentElement;
        private static FrameworkElement _rootVisual;
        private static Size _lastSize;

        #region Attached Dependency Properties

        #region DataContext Dependency Property

        /// <summary>
        /// Hidden dependency property that enables us to receive notifications when the source data context changes and 
        /// needs to be flushed into the context of the tooltip
        /// </summary>
        private static readonly DependencyProperty DataContextProperty =
            DependencyProperty.RegisterAttached("DataContext", typeof (object), typeof (ToolTipService),
                                                new PropertyMetadata(new PropertyChangedCallback(OnDataContextChanged)));

        /// <summary>
        /// When parent datacontext changes assign tooltip's datacontext to new datacontext
        /// </summary>
        public static void OnDataContextChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
        {            
            var owner = sender as FrameworkElement;
            ToolTip toolTip = GetToolTip(owner);

            Debug.Assert(!(ReferenceEquals(null, owner) ||
                           ReferenceEquals(null, toolTip)), "Unexpected null reference to attached FrameworkElement");

            toolTip.DataContext = owner.DataContext;
        }

        #endregion DataContext Dependency Property

        #region ToolTip Depdendency Property

        /// <summary>
        /// Identifies the ToolTipService.ToolTip dependency property.
        /// </summary>        
        public static readonly DependencyProperty ToolTipProperty = DependencyProperty.RegisterAttached("ToolTip",
                                                                                                        typeof (object),
                                                                                                        typeof (
                                                                                                            ToolTipService
                                                                                                            ),
                                                                                                        new PropertyMetadata
                                                                                                            (new PropertyChangedCallback
                                                                                                                 (OnToolTipPropertyChanged)));

        private static void OnToolTipPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var owner = (FrameworkElement) d;
            var newValue = (FrameworkElement) e.NewValue;

            if (e.OldValue != null)
            {
                UnregisterToolTip(owner);
            }
            if (newValue != null)
            {
                RegisterToolTip(owner, newValue);
            }
        }

        /// <summary>
        /// Gets the tooltip for an object.
        /// </summary>
        /// <param name="element">The UIElement from which the property value is read.</param>
        public static ToolTip GetToolTip(DependencyObject element)
        {
            if (element == null)
            {
                throw new ArgumentNullException("element");
            }
            return (ToolTip) element.GetValue(ToolTipProperty);
        }

        /// <summary>
        /// Sets the tooltip for an object.
        /// </summary>
        /// <param name="element">The UIElement to which the attached property is written.</param>
        /// <param name="value">The value to set.</param>
        public static void SetToolTip(DependencyObject element, ToolTip value)
        {
            SetToolTipInternal(element, value);
        }

        #endregion ToolTip Depdendency Property

        #region ToolTipObject Depdendency Property

        internal static readonly DependencyProperty ToolTipObjectProperty =
            DependencyProperty.RegisterAttached("ToolTipObject", typeof (object), typeof (ToolTipService), null);

        #endregion ToolTipObject Depdendency Property

        #endregion Attached Dependency Properties

        internal static Point MousePosition { get; set; }

        internal static FrameworkElement RootVisual
        {
            get
            {
                SetRootVisual();
                return _rootVisual;
            }
        }


        private static void OnElementIsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e)
        {
            ToolTipTimer timer = ElementsAndTimers[(UIElement) sender];
            if (!(bool) e.NewValue && timer.IsEnabled)
            {
                timer.StopAndReset();
            }
        }

        private static void OnTimerStopped(object sender, EventArgs e)
        {
            _currentToolTip.IsOpen = false;
        }

        private static void OnElementMouseEnter(object sender, MouseEventArgs e)
        {
            MousePosition = e.GetPosition(null);
            _currentElement = (UIElement) sender;
            _currentToolTip = ElementsAndToolTips[_currentElement];

            SetRootVisual();

            if (_currentToolTip.InitialDelay.Equals(0))
            {
                _currentToolTip.IsOpen = true;
            }

            ElementsAndTimers[_currentElement].StartAndReset();
        }

        private static void OnTimerTick(object sender, EventArgs e)
        {
            if (_currentToolTip.InitialDelay == ((ToolTipTimer) sender).CurrentTick)
            {
                _currentToolTip.IsOpen = true;
            }
        }

        private static void OnElementMouseLeave(object sender, MouseEventArgs e)
        {
            var element = (FrameworkElement) sender;
            ToolTipTimer timer = ElementsAndTimers[element];
            if (timer.IsEnabled)
            {
                timer.StopAndReset();
            }

            if (GetToolTip(element) != _currentToolTip)
            {
                return;
            }
            _currentToolTip.IsOpen = false;
        }

        private static void OnRootMouseMove(object sender, MouseEventArgs e)
        {
            MousePosition = e.GetPosition(null);
        }

        private static void OnRootVisualSizeChanged(object sender, SizeChangedEventArgs e)
        {
            if (_currentToolTip == null)
            {
                return;
            }
            if (_currentToolTip.Parent == null)
            {
                return;
            }

            PerformPlacement(_currentToolTip.HorizontalOffset, _currentToolTip.VerticalOffset);
        }

        private static void OnToolTipSizeChanged(object sender, SizeChangedEventArgs e)
        {
            _lastSize = e.NewSize;
            if (_currentToolTip.Parent != null)
            {
                PerformPlacement(_currentToolTip.HorizontalOffset, _currentToolTip.VerticalOffset);
            }
        }

        private static ToolTip ConvertToToolTip(object obj)
        {
            var toolTip = obj as ToolTip;

            if (toolTip != null )
            {
                return toolTip;
            }
            return new ToolTip {Content = obj};
        }

        private static void PerformPlacement(double horizontalOffset, double verticalOffset)
        {
            double num = MousePosition.Y + new TextBlock().FontSize + verticalOffset;
            double x = MousePosition.X + horizontalOffset;

            num = Math.Max(2.0, num);
            x = Math.Max(2.0, x);

            double actualHeight = RootVisual.ActualHeight;
            double actualWidth = RootVisual.ActualWidth;
            var rect = new Rect(x, num, _lastSize.Width, _lastSize.Height);
            var rect2 = new Rect(0.0, 0.0, actualWidth, actualHeight);
            rect2.Intersect(rect);

            var parentPopup = (Popup) _currentToolTip.Parent;
            if ((Math.Abs(rect2.Width - rect.Width) < 2.0) && (Math.Abs(rect2.Height - rect.Height) < 2.0))
            {
                parentPopup.VerticalOffset = num;
                parentPopup.HorizontalOffset = x;
            }
            else
            {
                if ((num + rect.Height) > actualHeight)
                {
                    num = (actualHeight - rect.Height) - 2.0;
                }
                if (num < 0.0)
                {
                    num = 0.0;
                }
                if ((x + rect.Width) > actualWidth)
                {
                    x = (actualWidth - rect.Width) - 2.0;
                }
                if (x < 0.0)
                {
                    x = 0.0;
                }
                parentPopup.VerticalOffset = num;
                parentPopup.HorizontalOffset = x;
                double num5 = ((num + rect.Height) + 2.0) - actualHeight;
                double num6 = ((x + rect.Width) + 2.0) - actualWidth;
                if ((num6 >= 2.0) || (num5 >= 2.0))
                {
                    num6 = Math.Max(0.0, num6);
                    num5 = Math.Max(0.0, num5);
                    PerformClipping(new Size(rect.Width - num6, rect.Height - num5));
                }
            }
        }

        private static void PerformClipping(Size size)
        {
            var child = VisualTreeHelper.GetChild(_currentToolTip, 0) as Border;
            if (child == null)
            {
                return;
            }

            if (size.Width < child.ActualWidth)
            {
                child.Width = size.Width;
            }
            if (size.Height < child.ActualHeight)
            {
                child.Height = size.Height;
            }
        }

        private static void UnregisterToolTip(UIElement owner)
        {
            owner.MouseEnter -= OnElementMouseEnter;
            owner.MouseLeave -= OnElementMouseLeave;
            if (owner.GetValue(ToolTipObjectProperty) == null)
            {
                return;
            }
            var tip = (ToolTip)owner.GetValue(ToolTipObjectProperty);
            if (tip.IsOpen)
            {
                tip.IsOpen = false;
            }
            owner.ClearValue(ToolTipObjectProperty);
        }

        private static void RegisterToolTip(FrameworkElement owner, FrameworkElement toolTip)
        {
            owner.MouseEnter += OnElementMouseEnter;
            owner.MouseLeave += OnElementMouseLeave;
            ToolTip tooltip = ConvertToToolTip(toolTip);


            if (ReferenceEquals(null, toolTip))
            {
                return;
            }

            toolTip.DataContext = owner.DataContext;

            owner.SetBinding(DataContextProperty, new Binding());


            owner.SetValue(ToolTipObjectProperty, tooltip);
            SetToolTipInternal(owner, tooltip);
        }


        private static void SetRootVisual()
        {
            if ((_rootVisual != null) || (Application.Current == null))
            {
                return;
            }

            _rootVisual = Application.Current.RootVisual as FrameworkElement;
            if (_rootVisual == null)
            {
                return;
            }

            _rootVisual.MouseMove += OnRootMouseMove;
            _rootVisual.SizeChanged += OnRootVisualSizeChanged;
        }

        private static void SetToolTipInternal(DependencyObject element, ToolTip value)
        {
            if (element == null)
            {
                throw new ArgumentNullException("element");
            }
            if (value == null)
            {
                ElementsAndToolTips.Remove(element);
                ElementsAndTimers.Remove(element);
                SetToolTipInternal(element, value);
                return;
            }
            var timer = new ToolTipTimer(value.ShowDuration, value.InitialDelay);

            timer.Tick += OnTimerTick;
            timer.Stopped += OnTimerStopped;
            value.SizeChanged += OnToolTipSizeChanged;

            var control = element as Control;

            if (control != null)
            {
                control.IsEnabledChanged += OnElementIsEnabledChanged;
            }

            if (ElementsAndTimers.ContainsKey(element))
            {
                ElementsAndTimers.Remove(element);
            }
            if (ElementsAndToolTips.ContainsKey(element))
            {
                ElementsAndToolTips.Remove(element);
            }

            ElementsAndTimers.Add(element, timer);
            ElementsAndToolTips.Add(element, value);

            element.SetValue(ToolTipProperty, value);
        }

      
    }
}
